package dungeon;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;

import condition.Condition;

import square.Direction;
import square.Rock;
import square.Square;
import square.TeleportationSquare;
import square.Temperature;
import square.TemperatureScale;
import square.TransparentSquare;
import be.kuleuven.cs.som.annotate.Basic;
import be.kuleuven.cs.som.annotate.Model;
import be.kuleuven.cs.som.annotate.Raw;
import border.Border;
import border.Door;
import border.Wall;

/**
 * A class for constructing a dungeon, containing squares of type E, that extends the Square type.
 *
 * @invar	The squares associated with this dungeon must be proper squares for this dungeon.
 * 			| hasProperSquares()
 * @invar	The positions associated with this dungeon must be proper positions for this dungeon.
 * 			| hasProperPositions()
 * @invar	The containing dungeon associated with this dungeon must be a proper containing dungeon for this dungeon.
 * 			| hasProperContainingDungeon()
 *
 * @param 	<E>
 * 			The type of the squares in this dungeon, E extends the square type.
 * 
 * @version	3.0
 * @author 	Inge Gielis <inge.gielis@student.kuleuven.be>
 * @author 	Annelies Ketelslegers <annelies.ketelslegers@student.kuleuven.be>
 */
public abstract class Dungeon<E extends Square> {
	
	/**
	 * Initialize this new dungeon with the given values as the maximal values for the coordinates
	 * of the positions of squares in this dungeon.
	 * 
	 * @param 	maxX
	 * 			The maximum value for the x coordinate of a position in a dungeon.
	 * @param 	maxY
	 * 			The maximum value for the Y coordinate of a position in a dungeon.
	 * @param 	maxZ
	 * 			The maximum value for the Z coordinate of a position in a dungeon.
	 * @post	The maximal values for coordinates of a position in a dungeon
	 * 			are set to the given values.
	 * 			| new.getMaxXCoordinate() == maxX && new.getMaxYCoordinate() == maxY && new.getMaxZCoordinate() == maxZ
	 * @throws 	IllegalArgumentException
	 * 			The given values for the maximal coordinates must all be positive.
	 * 			| maxX <0 || maxY <0 || maxZ <0
	 */
	public Dungeon(long maxX, long maxY, long maxZ){
		if(maxX <0 || maxY <0 || maxZ <0)
			throw new IllegalArgumentException("The given values must all be positive.");
		maxXCoordinate = maxX;
		maxYCoordinate = maxY;
		maxZCoordinate = maxZ;
	}

	/**
	 * Terminate this dungeon.
	 * 
	 * @post	This dungeon is terminated.
	 * 			|new.isTerminated()
	 */
	public abstract void terminate();

	/**
	 * Check whether this dungeon is terminated.
	 */
	@Basic
	@Raw
	public boolean isTerminated() {
		return this.isTerminated;
	}
	
	/**
	 * Variable registering whether this dungeon is terminated.
	 */
	protected boolean isTerminated;

	/**
	 * Return the composed dungeon that contains this dungeon.
	 * If no composed dungeon references this dungeon as a
	 * sub dungeon, null will be returned.
	 */
	public ComposedDungeon<E> getContainingDungeon(){
		return containingDungeon;
	}

	/**
	 * Return the root dungeon of this dungeon.
	 * 
	 * @return If this dungeon doesn't have a containing
	 * 			dungeon, this dungeon will be returned
	 * 			as the root dungeon.
	 * 			|if(getContainingDungeon() == null)
	 * 			| 	then result == this
	 * 			Otherwise, the root dungeon of the containing 
	 * 			dungeon will be returned.
	 * 			|result == containingDungeon.getRootDungeon();
	 */
	public Dungeon<E> getRootDungeon(){
		if(containingDungeon == null)
			return this;
		return containingDungeon.getRootDungeon();
	}

	/**
	 * Check whether this dungeon can have the given composed
	 * dungeon as its containing dungeon.
	 * 
	 * @param 	dungeon
	 * 			The composed dungeon to check.
	 * @return	If the dungeon is terminated, true if and
	 * 			only if the given composed dungeon is not effective.
	 * 			| if(isTerminated())
	 * 			|	then result == (dungeon == null)
	 * 			Otherwise, true if and only if the given composed
	 * 			dungeon is not yet terminated.
	 * 			| else result ==
	 * 			|	( ! dungeon.isTerminated())
	 */
	public boolean canHaveAsContainingDungeon(ComposedDungeon<E> dungeon){
		if(isTerminated())
			return dungeon == null;
		else
			return (dungeon == null || (! dungeon.isTerminated()));
	}

	/**
	 * Check whether this dungeon has a proper containing
	 * dungeon to which it is attached.
	 * 
	 * @return	True if and only if this dungeon can have the
	 * 			composed dungeon to which it is attached as its
	 * 			containing dungeon, and either this dungeon is
	 * 			not attached to a containing dungeon, or that
	 * 			composed dungeon references this dungeon as 
	 * 			one of its sub dungeons.
	 * 			| result ==
	 * 			|	(canHaveAsContainingDungeon(getContainingDungeon())
	 *			|	&& ((getContainingDungeon() == null || (getContainingDungeon().hasAsSubDungeon(this)))))
	 */
	public boolean hasProperContainingDungeon(){
		return(canHaveAsContainingDungeon(getContainingDungeon())
				&& ((getContainingDungeon() == null || (getContainingDungeon().hasAsSubDungeon(this)))));
	}

	/**
	 * Put this empty dungeon in the given composed dungeon
	 * at the given position.
	 * 
	 * @param 	containingDungeon
	 * 			The composed dungeon to which this dungeon
	 * 			must be attached.
	 * @param 	position
	 * 			The position at which this dungeon must
	 * 			be placed.
	 * @post	This dungeon is attached to the given 
	 * 			composed dungeon.
	 * 			|new.getContainingDungeon() == dungeon
	 * @post	The given composed dungeon has this
	 * 			dungeon as one of its sub dungeons.
	 * 			|(new dungeon).getSubDungeonAt(position) == this
	 * @throws	IllegalStateException
	 * 			This dungeon was already terminated.
	 * 			| this.isTerminated()
	 * @throws	IllegalArgumentException
	 * 			This dungeon can not have the given composed
	 * 			dungeon as the containing dungeon to be attached
	 * 			to, or already has a containing dungeon.
	 * 			| ! canHaveAsContainingDungeon(containingDungeon) 
	 * 			|	|| this.getContainingDungeon() != null
	 */
	
	public void putDungeonIn(ComposedDungeon<E> containingDungeon, Position position){
		if(this.isTerminated())
			throw new IllegalStateException("Terminated dungeon");
		if(this.getNbSquares() != 0)
			throw new IllegalStateException("Dungeon is not empty.");
		if( ! this.canHaveAsContainingDungeon(containingDungeon))
			throw new IllegalArgumentException("Unvalid containing dungeon.");
		if(this.getContainingDungeon() != null)
			throw new IllegalArgumentException("This dungeon already belongs to an other dungeon.");
		setContainingDungeon(containingDungeon);
		containingDungeon.addSubDungeonAt(this, position);
	}

	/**
	 * Set the containing dungeon to which this dungeon
	 * is attached to the given composed dungeon.
	 * 
	 * @param 	dungeon
	 * 			The composed dungeon to which this dungeon
	 * 			must be attached.
	 * @post	This dungeon is attached to the given composed
	 * 			dungeon
	 * 			| new.getContainingDungeon() == dungeon
	 * @throws	IllegalArgumentException
	 * 			This dungeon can not have the given composed 
	 * 			dungeon as the containing dungeon to be be
	 * 			attached to.
	 * 			| ! canHaveAsContainingDungeon(dungeon)
	 */
	protected void setContainingDungeon(ComposedDungeon<E> dungeon){
		if(!canHaveAsContainingDungeon(dungeon))
			throw new IllegalArgumentException("Unvalid containing dungeon.");
		this.containingDungeon = dungeon;
	}

	/**
	 * Variable referencing composed dungeon that contains this dungeons.
	 */
	private ComposedDungeon<E> containingDungeon;

	/**
	 * Return the maximum value for the X coordinate of a position in this dungeon.
	 */
	@Basic
	@Raw
	public long getMaxXCoordinate() {
		return maxXCoordinate; 
	}

	/**
	 * Return the maximum value for the Y coordinate of a position in this dungeon.
	 */
	@Basic
	@Raw
	public long getMaxYCoordinate() {
		return maxYCoordinate;
	}

	/**
	 * Return the maximum value for the Z coordinate of a position in this dungeon.
	 */
	@Basic
	@Raw
	public long getMaxZCoordinate() {
		return maxZCoordinate;
	}

	/**
	 * Check whether this dungeon can have the given position as one of its positions in the dungeon.
	 * 
	 * @param	position
	 * 			The position to check.
	 * @return 	False if the given position was not effective
	 * 			and if the x position is equal to the y position 
	 * 				and the y position is equal to the z position.
	 * 				So coordinates of the form (a,a,a) are not allowed.
	 * 			| if((position == null) 
	 * 			|	||((position.getX() == position.getY()) && (position.getY() == position.getZ())))
	 * 			| 		then result == false 
	 * @return	True if and only if the given values for respectively the x, y and z coordinate
	 * 			of a position in this dungeon are smaller than or equal to the maximum values for
	 * 			respectively the x, y and z coordinate of a position in this dungeon.
	 * 			| if(((position.getX() <= this.getMaxXCoordinate()) 
	 * 			|		&& (position.getY() <= this.getMaxYCoordinate()))
	 * 			|		&& (position.getZ() <= this.getMaxZCoordinate()))
	 * 			|	then result == true
	 */
	@Raw
	public boolean canHaveAsPosition(Position position){
		if((position == null)
					||((position.getX() == position.getY()) && (position.getY() == position.getZ())))
			return false;
		if(positionLiesInDungeon(position))
			return true;
		return false;
	}

	/**
	 * Check whether this dungeon has proper position associated with it.
	 * 
	 * @return	True if and only if this dungeon can have each of the positions in this dungeon
	 * 			as its valid positions, false otherwise.
	 * 			|result == 
	 * 			|	for each square in getSquares()
	 * 			|		canHaveAsPosition(getPositionOfSquare(square))
	 */
	public boolean hasProperPositions()
	{
		for(Iterator<E> i  = getSquares(null); i.hasNext(); ){
			E square = i.next();
			if(! canHaveAsPosition(getPositionOfSquare(square)))
				return false;
		}
		return true;
	}

	/**
	 * Check whether the given position lies in this dungeon.
	 * 
	 * @param position
	 * 			The position to check.
	 * @return	True if the coordinates of the position lie between 0
	 * 			and the maximal value of the coordinates of this dungeon.
	 * 			|result == 
	 * 			|	(((position.getX() <= this.getMaxXCoordinate()) 
	 * 			|	&& (position.getY() <= this.getMaxYCoordinate()))
	 * 			|	&& (position.getZ() <= this.getMaxZCoordinate()))
	 */
	public boolean positionLiesInDungeon(Position position){
		return(((position.getX() <= this.getMaxXCoordinate()) 
				&& (position.getY() <= this.getMaxYCoordinate()))
				&& (position.getZ() <= this.getMaxZCoordinate()));
	}

	/**
	 * Variable referencing the maximum value for the x coordinate of a position in a dungeon.
	 */
	protected final long maxXCoordinate;

	/**
	 * Variable referencing the maximum value for the y coordinate of a position in a dungeon.
	 */
	protected final long maxYCoordinate;

	/**
	 * Variable referencing the maximum value for the z coordinate of a position in a dungeon.
	 */
	protected final long maxZCoordinate;

	/**
	 * Return the number of slippery squares in this dungeon.	
	 */
	@Basic
	protected abstract int getNbSlipperySquares();

	/**
	 * Return the number of squares in this dungeon.
	 */
	@Basic
	public abstract int getNbSquares();

	/**
	 * Return the position at which the given square
	 * is registered as a square in this dungeon.
	 * 
	 * @param 	square
	 * 			The square to get the position for.
	 * @return	The result is a position of this dungeon.
	 * 			| hasSquareAt(result)
	 * @throws	IllegalArgumentException
	 * 			The given square is not a square for this dungeon.
	 * 			| ! hasAsSquare(square)
	 */
	public abstract Position getPositionOfSquare(E square) throws IllegalArgumentException;

	/**
	 * Return the square associated with this dungeon at the given position.
	 * 
	 * @param 	position
	 * 			The position of the square to be returned.
	 * @return	The result is a square of this dungeon.
	 * 			| hasAsSquare(result)	
	 * @throws	IllegalArgumentException
	 * 			If the given position is not a valid position for a square in this dungeon.
	 * 			| ! canHaveAsPosition(position)
	 */
	public abstract E getSquareAt(Position position) throws IllegalArgumentException;

	/**
	 * Returns an iterator for the collection of squares when given a condition.
	 * 
	 * @param 	condition
	 * 			The condition that must be satisfied for the squares to return.
	 * @return 	The iterator for the collection of squares when given a condition to be satisfied by all those squares. 
	 */
	public abstract Iterator<E> getSquares(final Condition condition);

	/**
	 * Return a map of squares that are located at the surrounding positions
	 * of the given position.
	 * 
	 * @param 	position
	 * 			The position to get the surrounding squares and positions of.
	 * @return	For each direction in the collection of possible directions
	 * 			if the position of the next square in that direction is effective 
	 * 			and the position of that next square is a valid position for this dungeon
	 * 			and the square at that position is effective,
	 * 			then that square is added to the collection of surrounding squares for the given position,
	 * 			with that square as a key and the position of that square as a value.
	 * 			| for each directionX in Direction.values()
	 * 			|	if(directionX.goStepInDirection(position)!= null 
	 * 			|		&& canHaveAsPosition(directionX.goStepInDirection(position)) 
	 * 			|		&& getSquareAt(directionX.goStepInDirection(position) != null))
	 * 			|		then result.get(getSquareAt(directionX.goStepInDirection(position))) == directionX.goStepInDirection(position)
	 */
	public HashMap<E,Position>getSurroundingSquares(Position position){
		HashMap<E,Position> result = new HashMap<E, Position>();
		for(Direction d: Direction.values()){
			if(d.goStepInDirection(position)!= null && canHaveAsPosition(d.goStepInDirection(position))){
				E neighbor = getSquareAt(d.goStepInDirection(position));
				if(neighbor != null){
					result.put(neighbor, d.goStepInDirection(position));
				}
			}
		}
		return result;
	}

	/**
	 * Check whether the dungeon can have the given square as one of its squares.
	 * 
	 * @param	square
	 * 			The square to check
	 * @return	False if the given square is noneffective
	 * 			or the given square is terminated 
	 * 			or the given square does not have proper borders attached to it
	 * 			| if(square == null
	 *			|	|| square.isTerminated()
	 *			|	|| ! square.hasProperBorders())
	 *			|		then result == false
	 * @return	False if the given square is a teleportation square 
	 * 			and the humidity of this square is not a valid humidity for this teleportation square 
	 * 			and the temperature for this square is not a valid temperature for this square, true otherwise.
	 * 			| if(square instanceof TeleportationSquare 
	 * 			|	&& ! ((TeleportationSquare.isValidHumidity(square.getHumidity()) 
	 * 			|	&& ((TeleportationSquare) square).isValidTemperature(square.getTemperature()))))
	 * 			|		then result == false
	 */
	@Raw
	public boolean canHaveAsSquare(E square){
		if(square == null
				|| square.isTerminated()
				|| ! square.hasProperBorders())
			return false;
		if(square instanceof TeleportationSquare && ! ((TeleportationSquare.isValidHumidity(square.getHumidity()) && ((TeleportationSquare) square).isValidTemperature(square.getTemperature()))))
			return false;
		return true;
		}

	/**
	 * Check whether this dungeon can have the given square at
	 * the given position.
	 * 
	 * @param 	square
	 * 			The square to check.
	 * @param 	position
	 * 			The position to check.
	 * @return 	False if this dungeon can not have the given square
	 * 			as a square or if this dungeon can not have the
	 * 			given position as a position.
	 * 			|if(!canHaveAsSquare(square) || ! canHaveAsPosition(position))
	 * 			|	then result == false
	 * 			Otherwise, false if there's an other square attached to this
	 * 			dungeon at the given position
	 * 			|if	(getSquareAt(position) != null && getSquareAt(position) != square)
	 * 			|	then result == false
	 * 			Otherwise, false if the given square is an instance of the class of teleportation squares 
	 * 			and one of the neighboring squares has a wall at the border with this square
	 * 			or false if one of the neighboring squares is an instance of the class of
	 * 			teleportation squares and the given square has a wall at the border with
	 * 			that neighbor. True otherwise.
	 * 			| for(Direction d: Direction.values()){
	 * 			|	E neighbor = getSquareAt(d.goStepInDirection(position));
	 * 			|		if((square instanceof TransparentSquare && neighbor.getBorder(d.getOpposite()) != null && neighbor.getBorder(d.getOpposite()) instanceof Wall)
	 *			|			|| (neighbor instanceof TransparentSquare && square.getBorder(d) != null && square.getBorder(d) instanceof Wall))
	 *			|				then result == false;
	 *
	 */
	public boolean canHaveAsSquareAt(E square, Position position){
		if(!canHaveAsSquare(square) || ! canHaveAsPosition(position))
			return false;
		if(getSquareAt(position) != null && getSquareAt(position) != square)
			return false;
		for(Direction d: Direction.values()){
			if(hasSquareAt(d.goStepInDirection(position))){
				E neighbor = getSquareAt(d.goStepInDirection(position));
				if((square instanceof TransparentSquare && neighbor.getBorder(d.getOpposite()) != null && neighbor.getBorder(d.getOpposite()) instanceof Wall)
						|| (neighbor instanceof TransparentSquare && square.getBorder(d) != null && square.getBorder(d) instanceof Wall))
					return false;
			}
		}
		return true;
	}

	/**
	 * Check whether it is possible to navigate from one square to the other in this dungeon.
	 * 
	 * @param 	start
	 * 			The square to start from.
	 * @param 	destination
	 * 			The square to go to.
	 * @return	True if the destination square is the same square as the start square.
	 * 			|if(start == destination)
	 * 			|	then result == true
	 * 			True if the start square is in the same space as the destination.
	 * 			|if(getSpace(start).contains(destination))
	 * 			|	then result == true
	 * 			True if it is possible to teleport from the start square to the destination.
	 * 			|if(start.canTeleportTo(destination))
	 * 			|	then result == true
	 * 			True if it is possible for any squareX in the union of 
	 * 			the neighbors of the start square 
	 * 			and the teleportation destinations of the start square
	 * 			and if it is possible to navigate from each squareX to the given destination. 
	 * 			|if(for squareX in (start.getNeighbors() U start.getTeleportationDestinations())
	 * 			|		if(hasAsSquare(squareX) && canNavigateTo(squareX,destination))
	 * 			|			then result == true
	 * @throws	IllegalArgumentException
	 * 			This dungeon can not have one of the given squares or if this dungeon
	 * 			doesn't have one of the given squares or if one of the given squares isn't
	 * 			a teleportation square.
	 * 			|(!canHaveAsSquare(start) || ! canHaveAsSquare(destination)
	 * 			|	|| ! hasAsSquare(start) || ! hasAsSquare(destination)
	 * 			|	|| !( start instanceof TeleportationSquare) || ! (destination instanceof TeleportationSquare)
	 */
	public boolean canNavigateTo(E start, E destination){
		if(!canHaveAsSquare(start) || ! canHaveAsSquare(destination))
			throw new IllegalArgumentException("Illegal squares");
		if(! hasAsSquare(start) || ! hasAsSquare(destination))
			throw new IllegalArgumentException("The squares do not belong to this dungeon.");
		if(!( start instanceof TeleportationSquare) || ! (destination instanceof TeleportationSquare))
			throw new IllegalArgumentException("Can only navigate between teleportationsquares");
		HashSet<TeleportationSquare> checkedSquares = new HashSet<TeleportationSquare>();
		ArrayList<TeleportationSquare> squaresToCheck = new ArrayList<TeleportationSquare>();
		squaresToCheck.add((TeleportationSquare) start);
		boolean foundDestination = (start == destination);
		while(foundDestination == false && !squaresToCheck.isEmpty()){
			TeleportationSquare squareToCheck = squaresToCheck.get(0);
			if(getSpace((E) squareToCheck).contains(destination))
				//Typecast to E: This can be done because squareToCheck is an element of a list containing teleportation squares (or a subtype)
				// and the squares in this list are squares from this dungeon, so the squares in this dungeon are of type teleportation square.
				//In other words this method is only used if type E of Dungeon<E> is type TeleportationSquare (or a subtype), so it is
				//verified that E is in fact TeleportationSquare. 
				//The cast is needed because the method getSpace(E) needs an argument of the E type, and since in this case E = TelportationSquare,
				//this cast can be done.
				foundDestination = true;
			else{
				for(Square square: getSpace((E) squareToCheck)){
					//Typecast to E: This can be done because squareToCheck is an element of a list containing teleportation squares (or a subtype)
					// and the squares in this list are squares from this dungeon, so the squares in this dungeon are of type teleportation square.
					//In other words this method is only used if type E of Dungeon<E> is type TeleportationSquare (or a subtype), so it is
					//verified that E is in fact TeleportationSquare. 
					//The cast is needed because the method getSpace(E) needs an argument of the E type, and since in this case E = TelportationSquare,
					//this cast can be done.
					if(! checkedSquares.contains(square) 
							&& ! squaresToCheck.contains(square))
						squaresToCheck.add((TeleportationSquare) square);
				}
			}
			if(squareToCheck.canTeleportTo((TeleportationSquare) destination))
				foundDestination = true;
			else{
				for(TeleportationSquare square: squareToCheck.getTeleportationDestinations())
					if(! checkedSquares.contains(square) && ! squaresToCheck.contains(square) && hasAsSquare(square))
						squaresToCheck.add(square);
			}
			squaresToCheck.remove(squareToCheck);
			checkedSquares.add(squareToCheck);
		}	
		return foundDestination;
	}

	/**
	 * Check whether this dungeon has proper squares associated with it.
	 * 
	 * @return	True if and only if this dungeon can have each of the squares at positions in this dungeon
	 * 			as its squares, false otherwise.
	 * 			| result == 
	 * 			|	for each square in getSquares()
	 * 			|		(canHaveAsSquare(square))
	 */
	public abstract boolean hasProperSquares();

	/**
	 * Check whether there is a square at the given position in this dungeon.
	 * 
	 * @param 	position
	 * 			The position to check.	
	 * @return	True if this dungeon has the square at the given position as one of its squares
	 * 			and the given position is a valid position for this dungeon.
	 * 			| result == hasAsSquare(getSquareAt(position)) && canHaveAsPosition(position)
	 */
	public abstract boolean hasSquareAt(Position position);

	/**
	 * Check whether the given square is attached to this dungeon.
	 * 
	 * @param 	square
	 * 			The square to check.
	 * @return	True if and only if the given square belongs to this dungeon,
	 * 			false otherwise.
	 * 			|result == canHaveAsSquare(square) && [square | square in getSquares()]

	 */
	public abstract boolean hasAsSquare(Square square);
	
	/**
	 * Add the given square at the given position as a square for this dungeon.
	 * 
	 * @param 	position
	 * 			The position on which the given square should be added to the dungeon.
	 * @param 	square
	 * 			The square to add to this dungeon.
	 * @post	The dungeon has the given square as its square at the given position
	 * 			| new.getSquareAt(position) == square
	 * @post 	The number of squares of this dungeon is incremented by 1.
	 * 			| new.getNbSquares() == this.getNbSquares() +1	
	 * @throws 	IllegalArgumentException
	 * 			If this dungeon cannot have the given square at the given position.
	 * 			| ! canHaveSquareAt(square, position)
	 */
	public abstract void addSquare(Position position, E square) throws IllegalArgumentException;

	/**
	 * Remove the given square from the set of squares attached to this dungeon.
	 * 
	 * @param 	square
	 * 			The square to be removed.
	 * @post	The given square will be removed from the collection of squares in this dungeon.
	 * 			| ! new.hasAsSquare(square)
	 * @post	The number of squares associated with this 
	 * 			dungeon is decremented by 1.
	 * 			| new.getNbSquares() == this.getNbSquares() -1
	 */
	public abstract void removeSquare(E square);

	/**
	 *  Remove the square for this dungeon at the given position.
	 * 
	 * @param 	position
	 * 			The position of the square to be removed.
	 * @post	This dungeon no longer has the square at 
	 * 			the given position as one of its squares.
	 * 			| ! new.hasSquareAt(position)
	 * @post	The number of squares of this dungeon is decremented by 1.
	 * 			| this.getNbSquares() -1 = new.getNbSquares()
	 */
	public abstract void removeSquareAt(Position position);

	/**
	 * Return the rocks that surround the given space.
	 * 
	 * @param 	newSpace
	 * 			The space of which we want to get the surrounding rocks.
	 * @return	The returned set is effective.
	 * 			| result != null
	 * @return 	Each rock in the resulting list has a square of the given space
	 * 			as its neighbor.
	 * 			|for rock in result && for some square in getSpace(squareInSpace)
	 * 			|	rock.hasAsNeighbour(square)
	 */
	protected HashSet<Rock> getSurroundingRocks(E square2) {
		HashSet<TeleportationSquare> newSpace = getSpace((E) square2);
		HashSet<Rock> rocks = new HashSet<Rock>();
		if(newSpace.isEmpty()){
			return rocks;
		}
		for(TeleportationSquare square : newSpace){
			for(Direction direction: Direction.values()){
				if(square.getBorder(direction)!= null){
					Square neighboringSquare = square.getBorder(direction).getOtherSquare(square);
					if((neighboringSquare instanceof Rock)
						&& ! rocks.contains(neighboringSquare))
						rocks.add((Rock) neighboringSquare);
				}
			}
		}
		return rocks;
	}

	/**
	 * Merge the given spaces. 
	 * Calculate the weighted average of the temperature and the humidity
	 * of both spaces and set the temperature and humidity of all the
	 * squares of both spaces to this new average values.
	 * 
	 * @param 	space1
	 * 			A space of squares that needs to be merged.
	 * @param 	space2
	 * 			A space of squares that needs to be merged.
	 * @return	A list, containing all squares of both spaces,
	 * 			and all squares of this new list have the same value for humidity and temperature.
	 * 			|for each square in space1
	 * 			|	result.contains(square)
	 * 			|for each square in space2
	 * 			| 	result.contains(square)
	 * 			|for each square in result
	 * 			| 	square.getTemperature() == result.get(0).getTemperature()
	 * 			|	&& square.getHumidity() == result.get(0).getHumidity()
	 * @throws 	IllegalArgumentException
	 * 			The given spaces may not be empty.
	 * 			|space1.isEmpty() || space2.isEmpty()
	 */
	protected HashSet<TeleportationSquare> mergeSpaces(HashSet<TeleportationSquare> spaceX, HashSet<TeleportationSquare> spaceY) {
		ArrayList<TeleportationSquare> space1 = new ArrayList<TeleportationSquare>();
		space1.addAll(spaceX);
		ArrayList<TeleportationSquare> space2 = new ArrayList<TeleportationSquare>();
		for(TeleportationSquare square:spaceY){
			if(!space1.contains(square))
				space2.add(square);
		}
		if(space1.isEmpty() || space2.isEmpty())
			throw new IllegalArgumentException("Empty lists.");
		Double newTemperatureValue = (space1.get(0).getTemperature().convertTo(TemperatureScale.CELCIUS).getTemperatureValue()*space1.size()
					+ space2.get(0).getTemperature().convertTo(TemperatureScale.CELCIUS).getTemperatureValue()*space2.size())/(space1.size() + space2.size());
		
		Temperature newTemperature = new Temperature(newTemperatureValue);
		BigDecimal newHumidity = ((space1.get(0).getHumidity().multiply(new BigDecimal(space1.size())).
				add(space2.get(0).getHumidity().multiply(new BigDecimal(space2.size()))))).
				divide(new BigDecimal(space1.size() + space2.size()),RoundingMode.HALF_UP);
		HashSet<TeleportationSquare> mergedList = new HashSet<TeleportationSquare>(space1);
		mergedList.addAll(space2);
		for(TeleportationSquare square:mergedList){
			square.setTemperature(newTemperature);
			square.setHumidity(newHumidity);
		}
		return mergedList;
	}
	
	/**
	 * Return all squares in the given subregion.
	 * 
	 * @param minX
	 * 			The minimal value for the x coordinate.
	 * @param maxX
	 * 			The maximum value for the x coordinate.
	 * @param minY
	 * 			The minimal value for the y coordinate.
	 * @param maxY
	 * 			The maximum value for the y coordinate.
	 * @param minZ
	 * 			The minimal value for the z coordinate.
	 * @param maxZ
	 * 			The maximum value for the z coordinate.
	 * @return	Each square in this dungeon that lies between 
	 * 			the given parameters will be in the returned map.
	 * 			|for each square in getAllSquares(ConditionDefault)
	 * 			|	Position p = getPositionOfSquare(square)
	 * 			|	if(minX <= p.getXCoordinate() <= maxX
	 * 			|		&& minY <= p.getYCoordinate() <= maxY
	 * 			|		&& minZ <= p.getZCoordinate() <= maxZ)
	 * 			|		result.contains(square) == true.
	 * @return	Each square in the returned map has a position that lies
	 * 			between the given parameters
	 * 			|for each square in result.values
	 * 			|	Position p = getPositionOfSquare(square)
	 * 			|	(minX <= p.getXCoordinate() <= maxX
	 * 			|		&& minY <= p.getYCoordinate() <= maxY
	 * 			|		&& minZ <= p.getZCoordinate() <= maxZ)
	 */
	protected HashMap<E,Position> getAllSquaresInSubRegion(long minX, long maxX, long minY, long maxY, long minZ, long maxZ){
		if(minX > maxX || minY > maxY || minZ > maxZ
				|| maxX > getMaxXCoordinate() || maxY > getMaxYCoordinate() || maxZ > getMaxZCoordinate())
			throw new IllegalArgumentException(" Illegal arguments.");
		long x = minX;
		long y = minY;
		long z = minZ;
		HashMap <E,Position> result = new HashMap<E, Position>();
		while(z <= maxZ){
			Position positionToCheck = new Position(x,y,z);
			if(canHaveAsPosition(positionToCheck) && hasSquareAt(positionToCheck))
				result.put(getSquareAt(positionToCheck),positionToCheck);
			if(x != maxX){
				x++;}
			else{
				x=minX;
				if(y!= maxY){
					y++;}
				else{
					y=minY;
					z++;
				}
			}	
		}
		return result;
	}
	
	
	
	/**
	 * Return all squares that lie at the border, indicated by the given direction.
	 * 
	 * @param direction
	 * 			The direction that indicates whicht squares to return.
	 * @effect	If direction is WEST, get all squares at position with x = getMaxXCoordinate()
	 * 			|getAllSquaresInSubRegion(getMaxXCoordinate, getMaxXCoordinate, 0, getMaxYCoordinate, 0, getMaxZCoordinate)
	 * @effect 	If direction is SOUTH, get all squares at position with y = getMaxYCoordinate().
	 * 			|getAllSquaresInSubRegion(0, getMaxXCoordinate, getMaxYCoordinate, getMaxYCoordinate, 0, getMaxZCoordinate)
	 * @effect 	If direction is CEILING, get all squares at position with z = getMaxZCoordinate()
	 * 			|getAllSquaresInSubRegion(0, getMaxXCoordinate, 0, getMaxYCoordinate, getMaxZCoordinate, getMaxZCoordinate)
	 * @effect 	If direction is EAST, get all squares at position with x = 0
	 * 			|getAllSquaresInSubRegion(0, 0, 0, getMaxYCoordinate, 0, getMaxZCoordinate)
	 * @effect 	If direction is NORTH, get all squares at position with y = 0
	 * 			|getAllSquaresInSubRegion(0, getMaxXCoordinate, 0, 0, 0, getMaxZCoordinate)
	 * @effect 	If direction is FLOOR, get all squares at position with z = 0
	 * 			|getAllSquaresInSubRegion(0, getMaxXCoordinate, 0, getMaxYCoordinate, 0, 0)			
	 */
	public Map<? extends Square, Position> getAllSquaresAtBorder(Direction direction){
		switch (direction){
		case WEST:
			return getAllSquaresInSubRegion(maxXCoordinate, maxXCoordinate, 0, maxYCoordinate, 0, maxZCoordinate);
		case SOUTH:
			return getAllSquaresInSubRegion(0, maxXCoordinate, maxYCoordinate, maxYCoordinate, 0, maxZCoordinate);
		case CEILING:
			return getAllSquaresInSubRegion(0, maxXCoordinate, 0, maxYCoordinate, maxZCoordinate, maxZCoordinate);
		case EAST:
			return getAllSquaresInSubRegion(0, 0, 0, maxYCoordinate, 0, maxZCoordinate);
		case NORTH:
			return getAllSquaresInSubRegion(0, maxXCoordinate, 0, 0, 0, maxZCoordinate);
		case FLOOR:
			return getAllSquaresInSubRegion(0, maxXCoordinate, 0, maxYCoordinate, 0, 0);
		default:
			return null;
		}
	}

	/**
	 * Return all squares that lie at the edge of this dungeon.
	 * 
	 * @effect 	Get all squares at position with x = getMaxXCoordinate()
	 * 			getAllSquaresInSubRegion(getMaxXCoordinate, getMaxXCoordinate, 0, getMaxYCoordinate, 0, getMaxZCoordinate)
	 * @effect 	Get all squares at position with y = getMaxYCoordinate()
	 * 			getAllSquaresInSubRegion(0, getMaxXCoordinate, getMaxYCoordinate, getMaxYCoordinate, 0, getMaxZCoordinate)
	 * @effect 	Get all squares at position with z = getMaxZCoordinate()
	 * 			getAllSquaresInSubRegion(0, getMaxXCoordinate, 0, getMaxYCoordinate, getMaxZCoordinate, getMaxZCoordinate)
	 * @effect 	Get all squares at position with x = 0
	 * 			getAllSquaresInSubRegion(0, 0, 0, getMaxYCoordinate, 0, getMaxZCoordinate)
	 * @effect 	Get all squares at position with y = 0
	 * 			getAllSquaresInSubRegion(0, getMaxXCoordinate, 0, 0, 0, getMaxZCoordinate)
	 * @effect 	Get all squares at position with z = 0
	 * 			getAllSquaresInSubRegion(0, getMaxXCoordinate, 0, getMaxYCoordinate, 0, 0)
	 */
	public HashMap<E,Position> getBoundarySquares(){
		HashMap<E,Position> result = new HashMap<E, Position>();
		result.putAll(getAllSquaresInSubRegion(maxXCoordinate, maxXCoordinate, 0, maxYCoordinate, 0, maxZCoordinate));
		result.putAll(getAllSquaresInSubRegion(0, maxXCoordinate, maxYCoordinate, maxYCoordinate, 0, maxZCoordinate));
		result.putAll(getAllSquaresInSubRegion(0, maxXCoordinate, 0, maxYCoordinate, maxZCoordinate, maxZCoordinate));
		result.putAll(getAllSquaresInSubRegion(0, 0, 0, maxYCoordinate, 0, maxZCoordinate));
		result.putAll(getAllSquaresInSubRegion(0, maxXCoordinate, 0, 0, 0, maxZCoordinate));
		result.putAll(getAllSquaresInSubRegion(0, maxXCoordinate, 0, maxYCoordinate, 0, 0));
		return result;
	}
	
	/**
	 * Open the given door of the given square in the dungeon.
	 * 
	 * @param 	square
	 * 			The square in this dungeon of which a door must be opened.
	 * @param 	door
	 * 			The door that must be opened.
	 * @effect 	If the given square is a square attached to this border 
	 * 			and if the given door is a door of this square
	 * 			and if there is a square on the other side of the door,
	 * 			then the 2 spaces on each side of the door will be merged.
	 * 			|if(hasAsSquare(square) && square.hasBorder(door) && door.getOtherSquare(square) != null)
	 * 			|	then mergeSpaces(getSpace(square), getSpace(door.getOtherSquare(square)))
	 * @post	If the given square is a square attached to this border 
	 * 			and if the given door is a door of this square
	 * 			and if there is a square on the other side of the door,
	 * 			then the door will be set open.
	 * 			| ! (new door).isClosed()
	 */
	public void openDoor(E square, Door door){
		if(hasAsSquare(square) && square.hasBorder(door) && door.getOtherSquare(square) != null && (square instanceof TeleportationSquare)){
			mergeSpaces(getSpace(square), getSpace((E) door.getOtherSquare(square)));
			door.open();
		}
	}

	/**
	 * Construct a border between two given squares, in a given direction. 
	 * The first square will be a new square to be added to this dungeon
	 * at the second square and a border will be constructed between the two, 
	 * in the given direction. 
	 * The second square will already be a part of this dungeon.
	 * 
	 * @param	newSquare
	 * 			The square that is not a part of this dungeon yet.
	 * @param	squareInDungeon
	 * 			The square that already is a part of this dungeon.
	 * @param	direction
	 * 			The direction in which the border should be constructed.
	 *  
	 * @effect	If the new square has a border (a wall or a door) 
	 * 			on the common borderline in the given direction 
	 * 			and the square in the dungeon does not, 
	 * 			a border is placed on the common borderline.
	 * 			| newSquare.getBorder(direction).constructBorder(squareInDungeon, direction.getOpposite())
	 * @effect	If the new square does not have a border 
	 * 			on the common borderline in the given direction 
	 * 			and the square in the dungeon does have a wall or a door, 
	 * 			a border is placed on the common borderline.
	 * 			| squareInDungeon.getBorder(direction.getOpposite()).constructBorder(newSquare, direction)
	 * @effect	If both squares have a border on the common borderline
	 * 			and the border of the new square is a wall,
	 * 			this wall will be the new border between the two squares. 
	 * 			The border of the square in this dungeon will be replaced 
	 * 			by the border of the new square.
	 * 			| squareInDungeon.getBorder(direction.getOpposite()).replaceWith(newSquare.getBorder(direction))
	 * @effect	If both squares have a border on the common borderline 
	 * 			and the border of the square in this dungeon is a wall,
	 * 			this wall will be the new border between the two squares. 
	 * 			The border of the new square will be replaced
	 * 			by the border of the square in this dungeon.
	 * 			| newSquare.getBorder(direction).replaceWith(squareInDungeon.getBorder(direction.getOpposite()))
	 * @effect	If both squares have a border, but not a wall, 
	 * 			then both squares have a door.
	 * 			Only if the door of the new square is open 
	 * 			and the door of the square in this dungeon is closed,
	 * 			the door of the square in this dungeon will be preserved. 
	 * 			The border of the new square will be replaced 
	 * 			by the border of the square in this dungeon.
	 * 			| newSquare.getBorder(direction).replaceWith(squareInDungeon.getBorder(direction.getOpposite()))
	 * @effect	If both squares have a border, but not a wall, 
	 * 			then both squares have a door.
	 * 			If the doors of both squares are open, 
	 * 			then the door of the new square will be preserved.
	 * 			The border of the square in this dungeon will be 
	 * 			replaced by the border of the new square.
	 * 			| squareInDungeon.getBorder(direction.getOpposite()).replaceWith(newSquare.getBorder(direction))
	 * 
	 * @throws	IllegalArgumentException
	 * 			To be able to construct a border between the two squares, 
	 * 			at least one of them must already have a border in the given direction.
	 * 			| ! newSquare.hasBorderInDirection(direction) && ! squareInDungeon.hasBorderInDirection(direction.getOpposite())
	 * @throws	IllegalArgumentException
	 * 			The given squares may not have an other square attached 
	 * 			to the border on the borderline of these squares.
	 * 			|(newSquare.getBorder(direction) != null && newSquare.getBorder(direction).getOtherSquare(newSquare)!=null)
	 *			|	|| (squareInDungeon.getBorder(direction.getOpposite()) != null && squareInDungeon.getBorder(direction.getOpposite()).getOtherSquare(squareInDungeon)!=null))
	 */
	protected void borderConstruction(E newSquare, E squareInDungeon, Direction direction) throws IllegalArgumentException{
		if(newSquare.getBorder(direction)==null 
				&&  squareInDungeon.getBorder(direction.getOpposite())==null)
			throw new IllegalArgumentException("At least one of the squares must have a border.");
		if((newSquare.getBorder(direction) != null && newSquare.getBorder(direction).getOtherSquare(newSquare)!=null)
			|| (squareInDungeon.getBorder(direction.getOpposite()) != null && squareInDungeon.getBorder(direction.getOpposite()).getOtherSquare(squareInDungeon)!=null))
			throw new IllegalArgumentException("One of the squares has an other square attached to the border that must be shared.");
		
		// The new square has a border (wall/door) on the common borderline and the square in the dungeon hasn't.
		if (newSquare.getBorder(direction)!=null 
				&& squareInDungeon.getBorder(direction.getOpposite())==null)
			newSquare.getBorder(direction).constructBorder(squareInDungeon, direction.getOpposite());
		
		// The new square has no border on the common borderline and the square in the dungeon has a border (wall/door).
		else if (newSquare.getBorder(direction)==null 
				&& squareInDungeon.getBorder(direction.getOpposite())!=null)
			squareInDungeon.getBorder(direction.getOpposite()).constructBorder(newSquare, direction);
		
		// Both squares have a border on the common borderline, if the new square has a Wall, this wall will be the new border-object between the 2 squares.
		else if(newSquare.getBorder(direction) instanceof Wall){
			squareInDungeon.getBorder(direction.getOpposite()).replaceWith(newSquare.getBorder(direction));
		}
	
		// If the new square has a border but not a wall on the common borderline and the square in the dungeon has a wall on the common borderline, the wall will be preserved.
		else if(squareInDungeon.getBorder(direction.getOpposite()) instanceof Wall){
			newSquare.getBorder(direction).replaceWith(squareInDungeon.getBorder(direction.getOpposite()));
		}
		
		// If both squares have a border but not a wall, both squares will have a door.
		// Only if the door of the new square is open and the door of the square in the dungeon is closed the new common border will be the border of the square in the dungeon.
		else if(! ((Door) newSquare.getBorder(direction)).isClosed() && ((Door) squareInDungeon.getBorder(direction.getOpposite())).isClosed()){
			newSquare.getBorder(direction).replaceWith(squareInDungeon.getBorder(direction.getOpposite()));
		}
		
		// Otherwise, the door of the new square will be kept.
		else{
			squareInDungeon.getBorder(direction.getOpposite()).replaceWith(newSquare.getBorder(direction));
		}
	}

	/**
	 * Return a list of all squares that are in reach of the given square.
	 * Spaces are created because squares border on each other and are not separated by an obstacle.
	 * An open door is not an obstacle.
	 * 
	 * @param 	square
	 * 			The square of which the space is to be found.
	 * @return 	A list of squares that belong to the same space as the given square.
	 * 			|for each squareX in result
	 * 			|	squareX.getSpace().contains(square)
	 * @return	The getSpaceLoop method is used to recursively find the space to which the given square belongs.
	 * 			The given ArrayList is a new, empty ArrayList.
	 * 			| result == getSpaceLoop(square, new ArrayList<Square>())
	 * @throws	IllegalArgumentException
	 * 			The given square is not a valid type of square for this dungeon,
	 * 			a teleportation square is needed.
	 * 			| ! (square instanceof TeleportationSquare)
	 * @throws	IllegalArgumentException
	 * 			The given square is not a square for this dungeon.
	 * 			| !hasAsSquare(square)
	 */
	public HashSet<TeleportationSquare> getSpace(E square) throws IllegalArgumentException{
		if(! (square instanceof TeleportationSquare))
			throw new IllegalArgumentException("The given square is not a valid type of square, a teleportation square is needed.");
		if(! hasAsSquare(square))
			throw new IllegalArgumentException("The given square is not a square of this dungeon.");
		return getSpaceLoop(square, new HashSet<TeleportationSquare>());
	}

	/**
	 * Check whether 2 squares are in the same space.
	 * Time complexity: O(n)
	 * 
	 * @param square1
	 * 			The first square to check.
	 * @param square2
	 * 			The second square to check
	 * @return	False if one of the squares isn't
	 * 			a square of this dungeon of if one of the
	 * 			squares is a rock.
	 * 			|if(!hasAsSquare(square1) || ! hasAsSquare(square2)
	 *			|		|| square1 instanceof Rock || square2 instanceof Rock)
	 *			|	then result == false
	 *			Else return if the space of square 1 contains
	 *			square 2.
	 *			|result == getSpace(square1).contains(square2)
	 */
	public boolean areInSameSpace(E square1, E square2){
		if(!hasAsSquare(square1) || ! hasAsSquare(square2)
				|| square1 instanceof Rock || square2 instanceof Rock)
			return false;
		return getSpace(square1).contains(square2);
	}

	/**
	 * Return the space to which the given square belongs.
	 *  
	 * @param	square
	 * 			The square of which the space is to be found.
	 * @param	currentSpace
	 * 			The space to which the given square belongs.
	 * @post	If the list of the squares in the current space does not already contain the given square,
	 * 			the given square is added to the list of squares in the current space.
	 * 			| if(! currentSpace.contains(square))
	 *			|	then currentSpace.add(square)
	 * @post	For every direction in the set of possible directions for a square in this dungeon,
	 * 			check if there is a border in that direction. 
	 * 			If that border is null or if that border is a door and that door is not closed, go a step in the given direction, to the neighboring position, 
	 * 			if that neighboring position is not null,
	 * 			if this dungeon has a square at the neighboring position and
	 * 			if the list of squares of the current space does not already contain the square at the neighboring position of the given square,
	 * 			then repeat until the list of squares in the current space contains all squares of that space.
	 * 			|for(Direction direction:Direction.values())
	 *			|	Border border = square.getBorder(direction)
	 *			|	if(border == null || (border instanceof Door && ! ((Door) border).isClosed()))
	 *			|		Position neighbouringPosition = direction.goStepInDirection(this.getPositionOfSquare(square))
	 *			|		if(neighbouringPosition != null)
	 *			|			if(hasSquareAt(neighbouringPosition))
	 *			|				if(! currentSpace.contains(getSquareAt(neighbouringPosition)))
	 *			|					currentSpace = getSpaceLoop(getSquareAt(direction.goStepInDirection(this.getPositionOfSquare(square))),currentSpace)
	 * @return	The space to which the given square belongs.
	 * 			| result == currentSpace
	 */
	@Model
	private HashSet<TeleportationSquare> getSpaceLoop(E square, HashSet<TeleportationSquare> currentSpace) throws IllegalArgumentException{
		if(!(square instanceof TeleportationSquare))
			throw new IllegalArgumentException();
		if(! currentSpace.contains(square))
			currentSpace.add((TeleportationSquare)square);
		
		for(Direction direction:Direction.values()){
			Border border = square.getBorder(direction);
			if(border == null || (border instanceof Door && ! ((Door) border).isClosed())){
				Position neighbouringPosition = direction.goStepInDirection(this.getPositionOfSquare(square));
				if(neighbouringPosition != null)
					if(hasSquareAt(neighbouringPosition))
						if(! currentSpace.contains(getSquareAt(neighbouringPosition)))
							currentSpace = getSpaceLoop(getSquareAt(direction.goStepInDirection(this.getPositionOfSquare(square))),currentSpace);
			}
		}
		return currentSpace;
		
	}
	
}